﻿using SIR.Color;
using SIR.Common.Condition;
using SIR.Entities.Implementations;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SIR.Services.ImageProcessing
{
	public static class Processing
	{
		private static IPredicate _pendingImagesCondition = new Predicate("StatusId", 0, PredicateOperator.EqualTo);
		/// <summary>
		/// Retrieves the next item from the queue if available
		/// </summary>
		/// <param name="queuedImage">The next image in the queue</param>
		/// <returns>True if there was an item in the queue</returns>
		private static bool CheckQueueForNextImage(out QueuedImage queuedImage)
		{
			// Search the DB for items in the queue
			var results = DAL.DataServer.Find<QueuedImage>(_pendingImagesCondition).Results.OrderByDescending(q => q.PriorityId);
			// Grab the first item from the queue
			queuedImage = results.FirstOrDefault();
			return queuedImage != null;
		}

		/// <summary>
		/// Process the next image in the queue
		/// </summary>
		/// <returns>Returns true while there are items still in the queue</returns>
		public static bool ProcessNextImage()
		{
			QueuedImage queuedImage = null;
			// Get the next item from the queue
			if (!CheckQueueForNextImage(out queuedImage))
				return false;
			// Set the status to processing 
			queuedImage.Status = Common.ProcessingStatus.Processing;
			DAL.DataServer.Save(queuedImage);
			// Get the image from the queue image object
			var image = ((Image)queuedImage.Image);
			// Set the image to processing
			image.Status = Common.ProcessingStatus.Processing;
			DAL.DataServer.Save(image);
			// Start analysing the image
			Task.WaitAll(AnalyzeImage(image, queuedImage));

			return true;
		}

		/// <summary>
		/// Removes the queued image and sets the processing status to complete
		/// </summary>
		/// <param name="queuedImage">The queued image to be removed from the queue</param>
		private static void RemoveFromQueue(QueuedImage queuedImage)
		{
			// Delete the queue image object
			queuedImage.IsDeleted = true;
			DAL.DataServer.Save(queuedImage);
			// Get the image associated to the queued item
			var image = ((Image)queuedImage.Image);
			// Set status to complete
			image.Status = Common.ProcessingStatus.Complete;
			DAL.DataServer.Save(image);
		}

		/// <summary>
		/// Reset any items that have failed during the processing cycle
		/// </summary>
		public static void FixedFailedQueueItems()
		{
			// Get all queued images with a failed status
			var failedItems = DAL.DataServer.Find<QueuedImage>(new Predicate("StatusId", (int)Common.ProcessingStatus.Failed, PredicateOperator.EqualTo)).Results;
			// If results were found
			if (failedItems.Any())
			{
				// for each failed item
				foreach (var failedItem in failedItems)
				{
					// Find the analysis record, if exists, and delete it
					var analysisItem = DAL.DataServer.Find<ImageAnalysis>(new Predicate("ImageId", failedItem.ImageId, PredicateOperator.EqualTo)).Results.FirstOrDefault();
					if (analysisItem != null)
					{
						analysisItem.IsDeleted = true;
						DAL.DataServer.Save(analysisItem);
					}
					// Reset the status to pending
					failedItem.Status = Common.ProcessingStatus.Pending;
					DAL.DataServer.Save(failedItem);
				}
			}
		}

		/// <summary>
		/// Analyze the image for colors and patterns
		/// </summary>
		/// <param name="image">The image to be analyzed</param>
		/// <param name="queuedImage">The queue reference for the image</param>
		/// <returns>An asynchronous task to complete the analysis on a different thread</returns>
		private static async Task AnalyzeImage(Image image, QueuedImage queuedImage)
		{
			// Return if the image has no image data
			if (image.Data == null)
				return;
			// Create a bitmat from the image bytes
			var bitmap = new System.Drawing.Bitmap(new System.IO.MemoryStream(image.Data.Data));

			// Initialize and save the analysis record
			var analysis = new ImageAnalysis();
			analysis.ImageId = image.Id;
			analysis = DAL.DataServer.Save<ImageAnalysis>(analysis).NewValue;

			try
			{
				// Process the bitmap for the pixels
				var pixelColorMatrix = await ProcessBitmap(analysis, bitmap);
				// Check the image for any patterns
				await IdentifyPatterns.ForImage(image.Id, pixelColorMatrix);
				// Remove the image from the queue
				RemoveFromQueue(queuedImage);
			}
			catch (Exception ex)
			{
				// If an exception occured, log the message
				DAL.DataServer.LogException(ex.Message + " " + ex.StackTrace);
				// Delete the analysis record
				analysis.IsDeleted = true;
				DAL.DataServer.Save(analysis);
				// Set the status to failed
				queuedImage.Status = Common.ProcessingStatus.Failed;
				DAL.DataServer.Save(queuedImage);
			}
		}

		/// <summary>
		/// Analyzes the bitmap for colors and returns the pixel matrix
		/// </summary>
		/// <param name="analysis">The parent image analysis record</param>
		/// <param name="bitmap">The bitmap for the image</param>
		/// <returns>A Matrix of a list of pixels within each section of the processed image</returns>
		private static async Task<ListMatrix2<PixelColor>> ProcessBitmap(ImageAnalysis analysis, System.Drawing.Bitmap bitmap)
		{
			// Run asynchronously
			return await Task.Run(() =>
			{
				// for locking
				object _lock = new object();
				// track duration
				var start = DateTime.Now;
				// split the image into 100 squares
				var split = 10;

				var items = new ListMatrix2<PixelColor>(split);
				// Determine the pixel size of each section
				var widthSize = bitmap.Width / split;
				var heightSize = bitmap.Height / split;

				var hueTasks = new List<Task<double>>();
				var pixelTasks = new List<Task<PixelColor>>();
				// Go pixel by pixel and build async tasks
				for (int ii = 0; ii < bitmap.Width; ++ii)
				{
					for (int jj = 0; jj < bitmap.Height; ++jj)
					{
						// Get the pixel
						var px = bitmap.GetPixel(ii, jj);
						// Create a task to get the hue
						hueTasks.Add(Task.Run<double>(() => { return px.GetHue(); }));
						// Create a task to create the pixel color object from the pixel
						pixelTasks.Add(Task.Run<PixelColor>(() => { return new PixelColor(px, ii, jj); }));
					}
				}

				// loop through pixel tasks
				foreach (var t in pixelTasks)
				{
					var px = t.Result;
					lock (_lock)
					{
						// Add the results to the matrix by converting the (x,y) pixel location to
						// which section it falls into
						items.Add(px, GetGroup(px.Point.X, widthSize, split), GetGroup(px.Point.Y, heightSize, split));
					}
				}

				// Create tasks to establishs spectrum objects from each section
				var tasks = new List<Task<ColorSpectrum>>();
				for (int ii = 0; ii < split; ++ii)
				{
					for (int jj = 0; jj < split; ++jj)
					{
						// Add the process section task
						tasks.Add(ProcessSection(analysis.Id, items[ii, jj], ii, jj));
					}
				}

				// Get all the the results from the spectrum tasks
				var allSpectrums = tasks.Select(t => t.Result);
				// Find the shortest average distance from all of the spectrums
				var shortestSpectrum = allSpectrums.OrderBy(spec => spec.AverageDistance).FirstOrDefault();

				// Get all of the hues from each pixel
				var hues = hueTasks.Select(t => t.Result).ToList();
				// Calculate the number of pixels in the image and set the average hue
				var count = bitmap.Width * bitmap.Height;
				analysis.AverageHue = hues.Sum() / count;
				// Set the shortest average distance color and  value
				analysis.ColorSpectrumDefinitionId = shortestSpectrum.DefinitionId;
				analysis.ShortestAverageDistance = shortestSpectrum.AverageDistance;
				// Calculate the standard hue deviation
				analysis.StandardHueDeviation = Math.Sqrt(hues.Select(h => { return Math.Pow(analysis.AverageHue - h, 2); }).Sum() / count);
				// Set the duration for how long the analysis took
				analysis.AnalysisDuration = (DateTime.Now - start).TotalSeconds;
				// Save the analysis results
				DAL.DataServer.Save<ImageAnalysis>(analysis);
				// Return the pixel matrix
				return items;
			});
		}

		/// <summary>
		/// Asynchronous task to create the color spectrums from a list of pixels
		/// </summary>
		/// <param name="analysisId">The identifier for the image analysis record</param>
		/// <param name="items">The list of pixels for this section</param>
		/// <param name="ii">The horizontal section for the spectrum</param>
		/// <param name="jj">The vertical section for the spectrum</param>
		/// <returns>The shortest spectrum for the entire section</returns>
		private static async Task<ColorSpectrum> ProcessSection(long analysisId, List<PixelColor> items, int ii, int jj)
		{
			// Run async
			return await Task.Run(() =>
			{
				ColorSpectrum shortestSpectrum = null;
				// Calculate all color spectrums for this section of the image
				var colorSpectrums = ProcessBitmapSection(items, ii, jj);
				// loop through the spectrums
				foreach (var colorSpectrum in colorSpectrums)
				{
					// Determine if this spectrum is the shortest spectrum
					if (shortestSpectrum == null || shortestSpectrum.AverageDistance > colorSpectrum.AverageDistance)
						shortestSpectrum = colorSpectrum;
					// Set the image analysis for the spectrum
					colorSpectrum.ImageAnalysisId = analysisId;
					// Save it
					DAL.DataServer.Save<ColorSpectrum>(colorSpectrum);
				}
				// Return the shortest spectrum
				return shortestSpectrum;
			});
		}

		/// <summary>
		/// Return the group or section that the value falls into based on the section size and the total number of groups
		/// </summary>
		/// <param name="value">The value to be checked</param>
		/// <param name="sectionSize">The size of each section</param>
		/// <param name="size">The total number of groups</param>
		/// <returns>Teh resulting section that the value belongs to</returns>
		public static int GetGroup(int value, int sectionSize, int size)
		{
			var index = 0;
			// Loop through each section
			for (; index < size - 1; ++index)
			{
				// If the value is less than the upperbounds of the section limit, then return that section
				if (value < (sectionSize * (index + 1)))
					return index;
			}
			// return the last section
			return index;
		}

		/// <summary>
		/// Returns the list of spectrums for the specified section of the image
		/// </summary>
		/// <param name="items">The pixels for this section of the image</param>
		/// <param name="sectionX">The horizontal section of the image</param>
		/// <param name="sectionY">The vertical section of the image</param>
		/// <returns>The array of color spectrums for this section (8)</returns>
		private static ColorSpectrum[] ProcessBitmapSection(List<PixelColor> items, int sectionX, int sectionY)
		{
			// Total number of pixels for this section
			var count = items.Count;
			// Sum of distances
			var sums = new double[8] { 0, 0, 0, 0, 0, 0, 0, 0 };

			// loop through each pixel
			foreach (var c in items)
			{
				// For each of the 8 color points, sum the distance
				for (int ii = 0; ii < 8; ++ii)
				{
					sums[ii] += c.DistanceFrom[ii];
				}
			}

			// Create all 8 spectrums with the correct color definition Id, section x, section y, and calculate the average distance
			ColorSpectrum[] spectrums = new[] 
				{
					new ColorSpectrum() { DefinitionId = 0, SectionX = sectionX, SectionY = sectionY, AverageDistance = sums[0] / count },
					new ColorSpectrum() { DefinitionId = 1, SectionX = sectionX, SectionY = sectionY, AverageDistance = sums[1] / count },
					new ColorSpectrum() { DefinitionId = 2, SectionX = sectionX, SectionY = sectionY, AverageDistance = sums[2] / count },
					new ColorSpectrum() { DefinitionId = 3, SectionX = sectionX, SectionY = sectionY, AverageDistance = sums[3] / count },
					new ColorSpectrum() { DefinitionId = 4, SectionX = sectionX, SectionY = sectionY, AverageDistance = sums[4] / count },
					new ColorSpectrum() { DefinitionId = 5, SectionX = sectionX, SectionY = sectionY, AverageDistance = sums[5] / count },
					new ColorSpectrum() { DefinitionId = 6, SectionX = sectionX, SectionY = sectionY, AverageDistance = sums[6] / count },
					new ColorSpectrum() { DefinitionId = 7, SectionX = sectionX, SectionY = sectionY, AverageDistance = sums[7] / count },
				};

			// For each of the colors
			for (int ii = 0; ii < 8; ++ii)
			{
				// Set the percent of the pixels that are within the average distance calculated abobe
				spectrums[ii].PercentWithinDistance = (items.Where(p => { return p.DistanceFrom[ii] <= spectrums[ii].AverageDistance; }).Count() / (count * 1.0)) * 100;
				// Calculate the average hue distance for each color
				spectrums[ii].AverageHueDistance = GetAverageHueDistance(ii, items);
			}
			// Return the new spectrums
			return spectrums;
		}

		/// <summary>
		/// Each "side" of the color wheel from each color, maximim distance is 180
		/// </summary>
		private static Dictionary<ColorIndex, Tuple<ushort, ushort>> _huePairs = new Dictionary<ColorIndex, Tuple<ushort, ushort>>()
		{
			{ ColorIndex.Red, new Tuple<ushort, ushort>(0, 180) },
			{ ColorIndex.Yellow, new Tuple<ushort, ushort>(60, 240) },
			{ ColorIndex.Magenta, new Tuple<ushort, ushort>(120, 300) },
			{ ColorIndex.Green, new Tuple<ushort, ushort>(120, 300) },
			{ ColorIndex.Cyan, new Tuple<ushort, ushort>(180, 360) },
			{ ColorIndex.Blue, new Tuple<ushort, ushort>(240, 60) },
		};

		/// <summary>
		/// Gets the average distance between all of the hues
		/// </summary>
		/// <param name="definitionId">The color definition Id</param>
		/// <param name="pixels">The a list of pixels from the image</param>
		/// <returns>The average distance in hue from the color</returns>
		private static double GetAverageHueDistance(int definitionId, IEnumerable<PixelColor> pixels)
		{
			// Ignore black and white
			if (definitionId == 0 || definitionId == 7)
				return 0;
			// get the hue bounds
			var pair = _huePairs[(ColorIndex)definitionId];
			// Select the minumum absolute value from both bounds of the hue, then get the average
			return pixels.Select(pc => Math.Min(Math.Abs(pc.Hue - pair.Item1), Math.Abs(pc.Hue - pair.Item2))).Average();
		}
	}
}
